#!/usr/bin/env python3
# https://www.zchen.info/archives/multithreading-with-pyqt5-and-qthread.html
# https://stackoverflow.com/questions/3170055/test-if-lists-share-any-items-in-python
import logging
import os
import platform
import queue
import re
import sys
import tempfile
import time
import traceback

import logzero
import stackprinter
import serial
import serial.tools.list_ports
from PyQt5.QtCore import QEvent, QObject, Qt, QThread, QTimer, pyqtSignal, pyqtSlot, QDateTime
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDesktopWidget,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QSizePolicy,
    QSpinBox,
    QSplitter,
    QTabWidget,
    QToolTip,
    QVBoxLayout,
    QWidget,
)

from devices.mengy_idin201f import IDIN201F_READ_QUEUE, idin201fGetWriteAreaName
from devices.tab10_idin201f import Tab_10_IDIN201F
from helpers.mengy_bytes_helper import bytes2Int, bytesPuttyPrint
import mengy_spac800
import mengy_spac800_bt
from modbus.mengy_modbus import CRC_OBJ, ask_03, calRecvLength, getErrCodeMean, write_05_06, write_10
from qt_custom_widget.qt_helper import addTooltipInSpinBox
from qt_custom_widget.qt_logWidget import MyDialog
from qt_custom_widget.qt_TimerMessageBox import TimerMessageBox
from qt_custom_widget.qt_TypeSelect import TypeSelector
import tabs_800.tab1_mea
import tabs_800.tab2_sys_para
import tabs_800.tab3_di_do
import tabs_800.tab4_prt_para_1
import tabs_800.tab5_prt_para_2
import tabs_800.tab6_event
import tabs_800.tab7_fault_info
import tabs_800.tab8_factory
import tabs_800.tab9_report

DI_FUNC_TIP_COLORS = ("#ff0000", "#951555", "#800000", "#d22780", "#800080", "#c85108")
BAUD_LIST = (1200, 2400, 4800, 9600, 19200, 38400, 56000, 57600, 115200)
MODBUS_FUNC_CODES = (0x03, 0x05, 0x06, 0x10)
RE_NUMBER = re.compile(r"\d+\.?\d+")
# Serial recv or send blink kirakira idle mS
BLINKIDLE = 100

# Window system version
if platform.release().upper() == "XP":
    PLATFORM_RELEASE = "XP"
    MAX_BLOCK_COUNT = 5000  # log widget block num
    SERIAL_CYCLE = 150  # Serial recv plus send idle mS
else:
    PLATFORM_RELEASE = "NXP"
    MAX_BLOCK_COUNT = 50000  # log widget block num
    SERIAL_CYCLE = 100  # Serial recv plus send idle mS

try:
    CRASH_DIR = os.path.join(tempfile.gettempdir(), "SPAC800")
    os.makedirs(CRASH_DIR, exist_ok=True)
    CRASH_FILE = "crash.log"
    CRASH_FILE_PATH = os.path.join(CRASH_DIR, CRASH_FILE)
    CRASH_LOGGER = logzero.setup_logger("SPAC800.crash", logfile=CRASH_FILE_PATH)
except Exception:
    CRASH_LOGGER = logzero.logger


# when app raises uncaught exception, print info
def trap_exc_during_debug(exc_type, exc_value, exc_traceback):
    CRASH_LOGGER.exception("crash", exc_info=(exc_type, exc_value, exc_traceback))
    CRASH_LOGGER.error("stackprinter | {}\n".format(stackprinter.format((exc_type, exc_value, exc_traceback))))
    traceback.print_exception(exc_type, exc_value, exc_traceback)


sys.excepthook = trap_exc_during_debug


class Worker(QObject):

    sig_working = pyqtSignal(int, bytes, bytes)
    sig_finished = pyqtSignal(int)
    sig_output = pyqtSignal(str, int)

    def __init__(self, idn: int, app, serialObj, sendQueue, deviceType):
        super().__init__()
        self.__id = idn
        self.__abort = False
        self.app = app
        self.serialObj = serialObj
        self.sendQueue = sendQueue
        self.deviceAddr = 1
        self.deviceType = 0

    def setDeviceAddr(self, deviceAddr):
        if isinstance(deviceAddr, int):
            self.deviceAddr = deviceAddr % 256

    @pyqtSlot()
    def work(self):
        cnt = 0
        thread_name = QThread.currentThread().objectName()
        thread_id = int(QThread.currentThreadId())
        self.sig_output.emit("start worker #{} from {}(#{})".format(self.__id, thread_name, thread_id), logging.DEBUG)
        if self.deviceType == 0:
            loopGroup = mengy_spac800.DISCT_RO
        else:
            loopGroup = mengy_spac800_bt.DISCT_RO
        while True:
            sendPack = b""
            try:
                try:
                    sendPack = self.sendQueue.get_nowait()
                except queue.Empty:
                    start, length = loopGroup[cnt % len(loopGroup)]
                    sendPack = ask_03(self.deviceAddr, start, length)
                    cnt += 1
                # self.logger.info('write | {}'.format(bytesPuttyPrint(sendPack)))
                if isinstance(sendPack, tuple):
                    loopGroup = sendPack
                elif isinstance(sendPack, bytes) and len(sendPack) > 0:
                    startTime = time.time()
                    self.serialObj.write(sendPack)
                    try:
                        result1 = self.serialObj.read(5)
                    except serial.serialutil.SerialException:
                        result1 = None
                    if result1 is None:
                        self.sig_working.emit(self.__id, sendPack, b"")
                    elif len(result1) < 5 or result1[1] not in MODBUS_FUNC_CODES:
                        self.sig_working.emit(self.__id, sendPack, result1)
                        if len(result1) > 0 and not (len(result1) == 5 and (result1[1] - 0x80) in MODBUS_FUNC_CODES):
                            print("should clear read buff {}".format(bytesPuttyPrint(result1)))
                            self.serialObj.reset_input_buffer()
                            if self.serialObj.isOpen():
                                self.serialObj.read(2048)
                            # if self.serialObj.isOpen():
                            #     self.serialObj.reset_input_buffer()
                    else:
                        result2 = self.serialObj.read(calRecvLength(sendPack) - 5)
                        if result2 and isinstance(result1, bytes):
                            try:
                                self.sig_working.emit(self.__id, sendPack, result1 + result2)
                            except AttributeError:
                                self.logger.error(
                                    "AttributeError in sig_working.emit\n{}".format(stackprinter.format())
                                )
                                raise
                    cycle = SERIAL_CYCLE - (time.time() - startTime) * 1000
                    if cycle > 0 and sendPack[1] == 0x03:
                        time.sleep(cycle / 1000)
                elif isinstance(sendPack, str):
                    m = re.compile(r"cmd_change_ser_addr(\d\d\d)").match(sendPack)
                    if m is not None:
                        self.setDeviceAddr(int(m.group(1)))
                else:
                    self.serialObj.read(2048)
            except serial.serialutil.SerialException as e:
                self.sig_output.emit(
                    "worker #{} serial.serialutil.SerialException {}".format(self.__id, repr(e)), logging.ERROR
                )
                logzero.logger.exception("serial.serialutil.SerialException")
                self.sig_working.emit(self.__id, b"", b"")
                break
            except Exception as e:
                self.sig_output.emit("worker #{} other Exception {}".format(self.__id, repr(e)), logging.ERROR)
                logzero.logger.exception("other Exception")
            self.app.processEvents()
            if self.__abort:
                self.sig_output.emit("worker #{} aborting".format(self.__id), logging.INFO)
                break
        self.sig_finished.emit(self.__id)

    def abort(self):
        self.sig_output.emit("worker #{} notified to abort".format(self.__id), logging.INFO)
        self.__abort = True


class SPAC800(QMainWindow):
    def __init__(self, app=None, logger=logzero.logger):
        super().__init__()
        self.app = app
        self.logger = logger
        self.initUI()

    def initUI(self):
        """initiates application UI"""
        self.statusBar()
        self.statusBar().setContentsMargins(0, 0, 0, 0)
        self.tboard = WidgetGallery(self, app=self.app, logger=self.logger)
        self.setCentralWidget(self.tboard)
        self.setWindowTitle("X-系列")
        self.setGeometry(200, 200, 1000, 600)
        # iconPath = os.path.join(os.path.abspath("./icons"), "iconfinder_telegram.png")
        iconPath = "./icons/iconfinder_telegram.png"
        # self.logger.info(f'iconPath {iconPath} {os.path.isfile(iconPath)}')
        self.setWindowIcon(QIcon(iconPath))
        # self.logger.info(f'self.width {self.width()} self.height() {self.height()}')
        self.show()
        self.center()
        # self.logger.info(f'self.width {self.width()} self.height() {self.height()}')

    def center(self):
        """centers the window on the screen"""
        screen = QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2 - 40)

    def cleanUp(self):
        self.logger.debug("invoke clean up")


class SerialStatusLabel(QLabel):
    """
    label tooltip update constantly
    """

    def __init__(self, parent=None, labelText="", logger=logzero.logger):
        super(SerialStatusLabel, self).__init__(parent)
        self._last_event_pos = None
        self.logger = logger
        self.label = QLabel(labelText)
        self.tootltipFunc = time.time

    def event(self, event):
        if event.type() == QEvent.ToolTip:
            self._last_event_pos = event.globalPos()
            # self.logger.info("QEvent.ToolTip | {}".format(self._last_event_pos))
            QToolTip.showText(self._last_event_pos, str(self.tootltipFunc()))
            return True
        elif event.type() == QEvent.Leave:
            self._last_event_pos = None
            # self.logger.info("QEvent.Leave | {}".format(self._last_event_pos))
            QToolTip.hideText()
        return QLabel.event(self, event)

    def setTooltipFunc(self, f):
        self.tootltipFunc = f


class WidgetGallery(QWidget):
    sig_abort_workers = pyqtSignal()

    def __init__(self, parent=None, app=None, logger=logzero.logger):
        super(WidgetGallery, self).__init__(parent)
        self.originalPalette = QApplication.palette()
        self.app = app
        self.logger = logger
        self.deviceType = 0
        self.userType = 0
        self.deviceSerAddr = 0x01
        self.serialObj = serial.Serial()
        self.serialObj.timeout = 3600 / self.serialObj.baudrate
        self.sendQueue = queue.Queue()
        # 串口接收数据收集
        self.serRecvDir = {}
        self.serStatistics = dict(send=0, recv=0, success=0, fail=0, fault=0, crcError=0, mess=0, noResp=0)
        # 串口日志显示过滤
        self.logPackFilter = list(MODBUS_FUNC_CODES)
        # 底栏
        self.statusbar = parent.statusBar()
        # 创建界面布局
        self.createSerialWidget()  # 串口布局
        self.createDeviceStatusBarWidget()  # 装置状态底栏
        # tab table
        self.tabs = []
        self.DataTabWidget = QTabWidget()
        self.createDataTab()

        # 串口布局置于状态底栏
        self.statusBarWidget = QWidget()
        layout = QHBoxLayout(self.statusBarWidget)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self.serialWidget)
        layout.addWidget(self.deviceStatusBarWidget)
        self.statusbar.addWidget(self.statusBarWidget)

        # 主显示及日志框
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        splitter = QSplitter(Qt.Vertical)
        splitter.addWidget(self.DataTabWidget)
        myDialog = MyDialog(logger=self.logger, maxBlockCount=MAX_BLOCK_COUNT)
        myDialog.setContentsMargins(0, 0, 0, 0)
        myDialog.setMinimumHeight(50)
        myDialog.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        splitter.addWidget(myDialog)
        splitter.setSizes((splitter.height() - 100, 150))
        layout.addWidget(splitter)

        # 串口工作线程
        QThread.currentThread().setObjectName("main")
        self.threads_num = 1
        self.__workers_done = 0
        self.__threads = []
        QTimer.singleShot(1000, self.switchDataTab)

    def keyPressEvent(self, e):
        """
        deal keyPressEvent
        """
        if e.key() == Qt.Key_F5:
            self.ctrlSerialPort(True)
        elif e.key() == Qt.Key_F6:
            self.ctrlSerialPort(False)

    def sendQueuePack(self, **kwargs):
        """发送数据包到队列"""
        addr = self.deviceSerAddr
        try:
            funCode = kwargs["funCode"]
            if funCode == 0x03:
                pack = ask_03(addr, kwargs["start"], kwargs["length"])
            elif funCode in (0x05, 0x06):
                pack = write_05_06(addr, funCode, kwargs["start"], kwargs["value"])
            elif funCode == 0x10:
                pack = write_10(addr, kwargs["start"], kwargs["data"])
            else:
                raise ValueError("fun code not in range (0x03, 0x05, 0x06, 0x10)")
            if funCode in MODBUS_FUNC_CODES:
                self.sendQueue.put(pack)
        except Exception:
            self.logger.exception("sendQueuePack exception by kwargs {}".format(kwargs))

    def createSerialWidget(self):
        """创建串口控制构件"""
        self.serialWidget = QWidget()
        serialLayout = QHBoxLayout(self.serialWidget)
        serialLayout.setContentsMargins(0, 0, 0, 0)
        serialLayout.setSpacing(0)

        self.serialPortNameComboBox = QComboBox()
        self.serialPortNameComboBox.addItems(self.getSerialPorts())
        self.serialPortNameComboBox.activated[str].connect(self.setSerialPortName)
        sserialNameLabel = QLabel("串口:")

        self.serialPortPushButton = QPushButton("打开串口")
        self.serialPortPushButton.setCheckable(True)
        self.serialPortPushButton.setChecked(False)
        self.serialPortPushButton.clicked[bool].connect(self.ctrlSerialPort)

        self.serialPortFreshButton = QPushButton("刷新串口")
        self.serialPortFreshButton.clicked[bool].connect(self.refreshSerialPort)

        serialLayout.addWidget(sserialNameLabel)
        serialLayout.addWidget(self.serialPortNameComboBox)
        serialLayout.addWidget(self.serialPortPushButton)
        serialLayout.addWidget(self.serialPortFreshButton)

        self.deviceSerAddrSb = QSpinBox()
        addTooltipInSpinBox(self.deviceSerAddrSb, (0, 255))
        self.deviceSerAddrSb.setValue(1)
        self.deviceSerAddrSb.valueChanged.connect(self.dealDeviceSerAddrChanged)

        self.serialPortRateComboBox = QComboBox()
        self.serialPortRateComboBox.addItems((str(i) for i in BAUD_LIST))
        self.serialPortRateComboBox.setCurrentIndex(BAUD_LIST.index(9600))
        self.serialObj.baudrate = 9600
        self.serialPortRateComboBox.activated[str].connect(self.setSerialPortRate)

        self.serialPortDataBitComboBox = QComboBox()
        self.serialPortDataBitComboBox.addItems(["8", "7", "6", "5"])
        self.serialPortDataBitComboBox.activated[str].connect(self.setSerialDataBit)

        self.serialPortStopBitComboBox = QComboBox()
        self.serialPortStopBitComboBox.addItems(["1", "1.5", "2"])
        self.serialPortStopBitComboBox.activated[str].connect(self.setSerialStopBit)

        self.serialPortParityComboBox = QComboBox()
        self.serialPortParityComboBox.setToolTip("N: 无校验\nE: 奇校验\nO: 偶校验\nM: 校验位始终为1\nS: 校验位始终为0")
        self.serialPortParityComboBox.addItems(["N", "E", "O", "M", "S"])
        self.serialPortParityComboBox.activated[str].connect(self.setSerialParity)

        self.serialBlinkStatistics = dict(send=0, recv=0)
        self.serialBlinkLblSend = SerialStatusLabel("发送", logger=self.logger)
        self.serialBlinkLblSend.setTooltipFunc(self.getSerStatisticsInfo)
        self.serialBlinkLblRecv = SerialStatusLabel("接收", logger=self.logger)
        self.serialBlinkLblRecv.setTooltipFunc(self.getSerStatisticsInfo)

        serialLayout.addWidget(QLabel("地址:"))
        serialLayout.addWidget(self.deviceSerAddrSb)
        serialLayout.addWidget(QLabel("波特率:"))
        serialLayout.addWidget(self.serialPortRateComboBox)
        serialLayout.addWidget(QLabel("数据位:"))
        serialLayout.addWidget(self.serialPortDataBitComboBox)
        serialLayout.addWidget(QLabel("停止位:"))
        serialLayout.addWidget(self.serialPortStopBitComboBox)
        serialLayout.addWidget(QLabel("校验位:"))
        serialLayout.addWidget(self.serialPortParityComboBox)
        serialLayout.addWidget(self.serialBlinkLblSend)
        serialLayout.addWidget(self.serialBlinkLblRecv)

    def getSerStatisticsInfo(self):
        """获取串口统计信息"""
        info = (
            "发送 {send} 接收 {recv} | {rate1:.2%}\n"
            "成功 {success} 失败 {fail} | {rate2:.2%}\n"
            "故障 {fault} 乱码 {mess} 无接收 {noResp}\n"
            "CRC校验错 {crcError}"
        ).format(
            rate1=self.serStatistics["recv"] / self.serStatistics["send"] if self.serStatistics["send"] > 0 else 1,
            rate2=self.serStatistics["fail"] / self.serStatistics["success"]
            if self.serStatistics["success"] > 0
            else 1,
            **self.serStatistics
        )
        return info

    def getSerialPorts(self):
        """获取串口列表"""
        serialPortList = [i for i in serial.tools.list_ports.comports()]
        if len(serialPortList) > 0:
            self.serialObj.port = serialPortList[0].device
            for serialPort in serialPortList:
                yield "{} | {}".format(serialPort.device, serialPort.description)

    def refreshSerialPort(self):
        """刷新串口列表"""
        self.serialPortNameComboBox.clear()
        self.serialPortNameComboBox.addItems(self.getSerialPorts())

    def setSerialPortName(self, serialPortName):
        """设置串口"""
        self.logger.info("serialPortName | {}".format(serialPortName))
        originPort = self.serialObj.port
        newPort = serialPortName.split("|")[0].strip()
        if originPort != newPort:
            self.logger.info("chage serial port | {} --> {}".format(originPort, newPort))
            if self.serialObj.isOpen():
                self.closeSerialPort()
            self.serialPortPushButton.setChecked(False)
            self.serialPortPushButton.setText("打开串口")
            self.serialObj.port = newPort

    def dealDeviceSerAddrChanged(self):
        """装置串口地址"""
        self.deviceSerAddr = self.deviceSerAddrSb.value()
        self.sendQueue.put("cmd_change_ser_addr{:03d}".format(self.deviceSerAddr))

    def setSerialPortRate(self, baudrate):
        """串口波特率"""
        self.logger.info("baudrate | {}".format(baudrate))
        self.serialObj.baudrate = int(baudrate)
        self.serialObj.timeout = 3600 / self.serialObj.baudrate

    def setSerialDataBit(self, bytesize):
        """串口数据位长度"""
        self.logger.info("bytesize | {}".format(bytesize))
        self.serialObj.bytesize = int(bytesize)

    def setSerialStopBit(self, stopbits):
        """串口停止位"""
        self.logger.info("stopbits | {}".format(stopbits))
        self.serialObj.stopbits = float(stopbits)

    def setSerialParity(self, parity):
        """串口校验位"""
        self.logger.info("parity | {}".format(parity))
        self.serialObj.parity = parity

    def ctrlSerialPort(self, pressed):
        """打开/关闭串口"""
        if pressed:
            self.openSerialPort()
            self.onTabSwitch()
            self.start_workers()
        else:
            self.closeSerialPort()

    def openSerialPort(self):
        """尝试打开串口"""
        self.logger.info("open serial port | {}".format(self.serialObj))
        try:
            self.serialObj.open()
        except Exception:
            self.logger.exception("open port failed")
            self.serialPortPushButton.setText("打开串口")
            self.serialPortPushButton.setChecked(False)
        else:
            self.serialPortPushButton.setChecked(True)
            self.serialPortPushButton.setText("关闭串口")
            self.serialPortFreshButton.setDisabled(True)

    def closeSerialPort(self):
        """尝试关闭串口"""
        self.logger.info("close serial port | {}".format(self.serialObj))
        try:
            # self.serialPortPushButton.setDisabled(True)
            self.abort_workers()
            self.serialObj.close()
        except Exception:
            self.logger.exception("close port failed")
            self.serialPortPushButton.setChecked(True)
        else:
            self.serialPortFreshButton.setDisabled(False)
            self.serialPortPushButton.setText("打开串口")
            self.serialPortPushButton.setChecked(False)
            # self.printSerRecv()

    def changeBlinkLabelColor(self, lbl, status=0):
        """修改闪烁标签颜色"""
        if lbl == "send":
            if status == 0:
                self.serialBlinkLblSend.setStyleSheet("background-color : white; color : #3d3d3d;")
            else:
                self.serialBlinkLblSend.setStyleSheet("background-color : red; color : #3d3d3d;")
        elif lbl == "recv":
            if status == 0:
                self.serialBlinkLblRecv.setStyleSheet("background-color : white; color : #3d3d3d;")
            else:
                self.serialBlinkLblRecv.setStyleSheet("background-color : green; color : #3d3d3d;")

    def kirakiraFunc(self, lbl):
        """收发闪烁效果"""
        self.changeBlinkLabelColor(lbl, 1)
        now = time.time() * 1000
        if now <= self.serialBlinkStatistics[lbl] + BLINKIDLE:
            return
        else:
            self.serialBlinkStatistics[lbl] = now
        QTimer.singleShot(BLINKIDLE, lambda: self.changeBlinkLabelColor(lbl, 0))

    def start_workers(self):
        """启动串口收发线程"""
        self.dealDeviceSerAddrChanged()  # 先读取串口目标地址
        self.logger.info("Starting {} threads".format(self.threads_num))
        if not isinstance(self.currentTab, Tab_10_IDIN201F):
            if self.deviceType == 0:
                for start, length in mengy_spac800.DISCT_RO + mengy_spac800.DISCT_RW:
                    self.sendQueuePack(funCode=0x03, start=start, length=length)
            else:
                for start, length in mengy_spac800_bt.DISCT_RO + mengy_spac800_bt.DISCT_RW:
                    self.sendQueuePack(funCode=0x03, start=start, length=length)
        else:
            for start, length in IDIN201F_READ_QUEUE:
                self.sendQueuePack(funCode=0x03, start=start, length=length)
            self.sendQueue.put(IDIN201F_READ_QUEUE)
        self.__workers_done = 0
        self.__threads = []
        for idx in range(self.threads_num):
            worker = Worker(
                idx, app=self.app, serialObj=self.serialObj, sendQueue=self.sendQueue, deviceType=self.deviceType
            )
            thread = QThread()
            thread.setObjectName("thread_" + str(idx))
            self.__threads.append((thread, worker))
            worker.moveToThread(thread)
            worker.sig_working.connect(self.on_worker_working)
            worker.sig_finished.connect(self.on_worker_finished)
            worker.sig_output.connect(self.on_worker_output)
            self.sig_abort_workers.connect(worker.abort)
            thread.started.connect(worker.work)
            thread.start()

    @pyqtSlot()
    def abort_workers(self):
        self.logger.info("Asking each worker to abort")
        self.sig_abort_workers.emit()

    @pyqtSlot(int, bytes, bytes)
    def on_worker_working(self, worker_id: int, data_send: bytes, data_recv: bytes):
        # self.logger.info("worker #{}: {}".format(worker_id, data))
        try:
            self.decodeSerialPack(data_send, data_recv)
        except Exception:
            self.logger.exception("decodeSerialPack exception")

    @pyqtSlot(int)
    def on_worker_finished(self, worker_id):
        self.logger.info("-- Worker #{} done".format(worker_id))
        self.__workers_done += 1

        thread, _ = self.__threads[worker_id]
        thread.quit()
        thread.wait()

        if self.__workers_done == self.threads_num:
            self.logger.info("No more workers active")
            self.logger.info("All threads exited")
            # self.serialPortPushButton.setDisabled(False)

    @pyqtSlot(str, int)
    def on_worker_output(self, msg, level):
        if level == logging.ERROR:
            self.logger.error(msg)
        elif level == logging.WARNING:
            self.logger.warning(msg)
        elif level == logging.INFO:
            self.logger.info(msg)
        elif level == logging.DEBUG:
            self.logger.debug(msg)
        else:
            self.logger.info(msg)

    def updateSerRecv(self, addr: int, data_recv: bytes):
        changeAddrList = []
        for i in range(data_recv[2] // 2):
            key = addr + i
            newData = data_recv[2 * i + 3 : 2 * i + 5]
            if newData != self.serRecvDir.get(key):
                self.serRecvDir[key] = newData
                changeAddrList.append(key)
        self.dealUpdateWidget(changeAddrList)

    def printSerRecv(self):
        for k in sorted(self.serRecvDir.keys()):
            self.logger.info("0x{:04X} --> {}".format(k, bytesPuttyPrint(self.serRecvDir[k])))

    # 解析数据包
    def decodeSerialPack(self, data_send: bytes, data_recv: bytes):
        if len(data_send) == 0:  # serial write error
            self.closeSerialPort()  # 关闭串口
            # self.refreshSerialPort() # 刷新串口
            # 显示提示信息
            # self.statusbar.showMessage("串口写入出错 尝试断开")
            messageBox = QMessageBox(self)
            messageBox.setIcon(QMessageBox.Critical)
            messageBox.setWindowTitle("错误")
            messageBox.setText("串口故障")
            messageBox.setInformativeText("请刷新串口")
            messageBox.setStandardButtons(QMessageBox.Ok)
            messageBox.exec_()
            return
        # flag Data set
        calLength = calRecvLength(data_send)
        addr = bytes2Int(data_send[2:4])
        if (not isinstance(self.currentTab, Tab_10_IDIN201F)) or addr == 0x0602:
            if self.deviceType == 0:
                areaName = mengy_spac800.getWriteAreaName(addr)
            else:
                areaName = mengy_spac800_bt.getWriteAreaName(addr)
        else:
            areaName = idin201fGetWriteAreaName(addr)
        recvLength = len(data_recv)
        checkLength = calLength == recvLength
        checkCRC = recvLength > 0 and CRC_OBJ.check_crc_bytes(data_recv)
        # kirakira func
        self.kirakiraFunc("send")
        self.serStatistics["send"] += 1
        if recvLength > 0:
            self.serStatistics["recv"] += 1
            self.kirakiraFunc("recv")
        # log pack
        logFunc = self.logger.info
        msg = "{} -> {}".format(bytesPuttyPrint(data_send), bytesPuttyPrint(data_recv))
        if data_send[1] != 0x03:
            msg = "WRITE_{:02X} {}".format(data_send[1], msg)
        # 长度判定处理
        if not checkLength:
            logFunc = self.logger.error
            msg = "应收长度 {:^3d} 实收长度 {:^3d} | {}".format(calLength, recvLength, msg)
        # CRC 判定处理
        if checkCRC and not checkLength:
            msg = "故障 {} | {}".format(getErrCodeMean(data_send, data_recv), msg)
        if not checkCRC:
            logFunc = self.logger.error
            if checkLength:
                msg = "CRC 校验错误 | {}".format(msg)
            elif recvLength > 0:
                msg = "乱码 | {}".format(msg)
            else:
                msg = "无接收 | {}".format(msg)
        if data_send[1] in self.logPackFilter or logFunc == self.logger.error:  # 日志包过滤显示
            logFunc(msg)
        if data_send[1] == 0x06 and addr == 0x0E20:  # recovery from adjust
            self.autoAujTab.recoverFromAdj()
        # message box and update ser recv dict
        if checkLength and checkCRC:
            self.serStatistics["success"] += 1
            if data_send[1] == 0x03 and data_recv[1] == 0x03:  # 收发都是03功能码
                startAddr = bytes2Int(data_send, 2, 2)  # 计算起始地址
                self.updateSerRecv(startAddr, data_recv)  # 更新缓存
            elif (data_send[1] == 0x10 and data_recv[1] == 0x10) or (data_send[1] == 0x06 and data_recv[1] == 0x06):
                if addr not in (0x0910, 0x0911, 0x0912):
                    self.logger.info("write addr is {}".format(addr))
                    messageBox = TimerMessageBox(parent=self, timeout=1)
                    messageBox.setIcon(QMessageBox.Information)
                    messageBox.setWindowTitle("信息")
                    messageBox.setInformativeText("写入成功")
                    messageBox.setText(areaName)
                    messageBox.setStandardButtons(QMessageBox.NoButton)
                    messageBox.exec_()
        else:
            self.serStatistics["fail"] += 1
            if recvLength == 0:
                self.serStatistics["noResp"] += 1
            else:
                if checkCRC:
                    self.serStatistics["fault"] += 1
                elif checkLength:
                    self.serStatistics["crcError"] += 1
                else:
                    self.serStatistics["mess"] += 1

            if data_send[1] != 0x03 and addr not in (0x0910, 0x0911, 0x0912):
                messageBox = QMessageBox(self)
                messageBox.setIcon(QMessageBox.Critical)
                if data_send[1] in (0x05, 0x06, 0x10):
                    text = "写入错误"
                else:
                    text = "其他功能码错误"
                messageBox.setWindowTitle(text)
                if checkCRC:  # 报文CRC匹配 但是长度不足
                    informText = getErrCodeMean(data_send, data_recv)
                    if (
                        (addr >= 0x0000 and addr <= 0x0003)
                        or (addr >= 0x0200 and addr < 0x0900)
                        or (addr >= 0x0B20 and addr <= 0x0B24)
                    ):
                        informText = "".join((informText, "\n", "（检查就地/远方信号）"))
                    messageBox.setInformativeText(informText)
                else:  # CRC不正确
                    if recvLength == 0:
                        messageBox.setInformativeText("无接收")
                    else:
                        messageBox.setInformativeText("CRC 校验错误")
                messageBox.setText(areaName)
                messageBox.exec_()

            if data_send[1] == 0x06 and addr == 0x0E22:  # worlk model read
                self.autoAujTab.dealDeviceWorkModel()

    def createDeviceStatusBarWidget(self):
        """创建装置状态底栏"""
        self.deviceStatusBarWidget = QWidget()
        layout = QHBoxLayout(self.deviceStatusBarWidget)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.serialLogFilterCBs = []
        layout.addWidget(QLabel("日志包过滤:"))
        for i in MODBUS_FUNC_CODES:
            sfc = QCheckBox("{:02X}".format(i))
            sfc.clicked[bool].connect(self.dealSerialLogFilterCheckBox)
            sfc.setChecked(True)
            layout.addWidget(sfc)
            self.serialLogFilterCBs.append(sfc)

        ws = (layout.itemAt(i).widget() for i in range(layout.count()))
        for w in ws:
            if isinstance(w, QLabel):
                w.setAlignment(Qt.AlignCenter)
        layout.addStretch(1)

    def dealSerialLogFilterCheckBox(self, clicked):
        """日志包过滤处理"""
        sender = self.sender()
        if sender not in self.serialLogFilterCBs:
            return
        funCode = MODBUS_FUNC_CODES[self.serialLogFilterCBs.index(sender)]
        if clicked and funCode not in self.logPackFilter:
            self.logPackFilter.append(funCode)
        if not clicked and funCode in self.logPackFilter:
            self.logPackFilter.remove(funCode)

    def switchDataTab(self):
        """切换表格"""
        typeSelect = TypeSelector(self, self.logger)
        typeSelect.exec_()
        self.logger.info("select result is {}".format(typeSelect.result))
        if typeSelect.result is not None:
            self.deviceType, self.userType = typeSelect.result
        self.clearDataTab()
        if self.deviceType == 1:
            self.modifyDataTab_BT()
            self.parent().setWindowTitle("B-系列")
        else:
            self.deviceType = 0
            self.createDataTab()
        # 更新夫组件标题
        self.genTitleTimer()

    def genTitleTimer(self):
        """制造定时器更新标题"""
        title_timer = QTimer(self)
        title_timer.setInterval(100)
        title_timer.timeout.connect(self.updateParentTitle)
        # https://stackoverflow.com/questions/4600068/qtime-qtimer-timeout-driven-stopwatch-has-high-cpu-usage
        title_timer.start(1000)

    def updateParentTitle(self):
        """更新标题的具体逻辑"""
        if self.deviceType == 0:
            prefix = "X-系列"
        else:
            prefix = "B-系列"
        timeStr = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
        self.parent().setWindowTitle("{}        {}".format(prefix, timeStr))

    def clearDataTab(self):
        """清除所有Tab"""
        self.logger.info("clear all tab")
        self.DataTabWidget.blockSignals(True)
        for i in range(len(self.tabs)):
            self.DataTabWidget.removeTab(0)
        self.DataTabWidget.blockSignals(False)
        self.tabs.clear()

    def modifyDataTab_BT(self):
        self.logger.info("add bt tabs in data tab")
        tab1 = tabs_800.tab1_mea.Tab1_Mea_BT(serRecvDir=self.serRecvDir, logger=self.logger)
        tab2 = tabs_800.tab2_sys_para.Tab2_Sys_Para_BT(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab3 = tabs_800.tab3_di_do.Tab3_DI_DO_BT(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab4 = tabs_800.tab4_prt_para_1.Tab4_Prt_Para_BT(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab6 = tabs_800.tab6_event.Tab_6_Event_BT(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab7 = tabs_800.tab7_fault_info.Tab_7_Fault_Info_BT(serRecvDir=self.serRecvDir, logger=self.logger)
        tab8 = tabs_800.tab8_factory.Tab_8_Factory_BT(
            serialObj=self.serialObj, serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab9 = tabs_800.tab9_report.Tab_9_Report_BT(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, app=self.app, logger=self.logger
        )
        tab10 = Tab_10_IDIN201F(serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger)

        self.currentTab = tab1
        self.lastCurrentTab = tab1
        self.eventTab = tab6
        self.autoAujTab = tab8
        self.DataTabWidget.addTab(tab1, "测量监视")
        self.tabs.append(tab1)
        self.DataTabWidget.addTab(tab2, "通信及数据")
        self.tabs.append(tab2)
        self.DataTabWidget.addTab(tab3, "开入开出")
        self.tabs.append(tab3)
        self.DataTabWidget.addTab(tab4, "保护参数")
        self.tabs.append(tab4)
        self.DataTabWidget.addTab(tab6, "事件记录")
        self.tabs.append(tab6)
        self.DataTabWidget.addTab(tab7, "故障信息")
        self.tabs.append(tab7)
        if self.userType > 0:
            self.DataTabWidget.addTab(tab8, "厂家设置")
            self.tabs.append(tab8)
            self.DataTabWidget.addTab(tab9, "测试报告")
            self.tabs.append(tab9)
            self.DataTabWidget.addTab(tab10, "测量板测试")
            self.tabs.append(tab10)

    # 创建表格
    def createDataTab(self):
        # self.DataTabWidget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Ignored)
        tab1 = tabs_800.tab1_mea.Tab1_Mea(serRecvDir=self.serRecvDir, logger=self.logger)
        tab2 = tabs_800.tab2_sys_para.Tab2_Sys_Para(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab3 = tabs_800.tab3_di_do.Tab3_DI_DO(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab4 = tabs_800.tab4_prt_para_1.Tab4_Prt_Para_1(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab5 = tabs_800.tab5_prt_para_2.Tab5_Prt_Para_2(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab6 = tabs_800.tab6_event.Tab_6_Event(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )
        tab7 = tabs_800.tab7_fault_info.Tab_7_Fault_Info(serRecvDir=self.serRecvDir, logger=self.logger)
        tab8 = tabs_800.tab8_factory.Tab_8_Factory(
            serialObj=self.serialObj, serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger
        )

        tab9 = tabs_800.tab9_report.Tab_9_Report(
            serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, app=self.app, logger=self.logger
        )
        tab10 = Tab_10_IDIN201F(serRecvDir=self.serRecvDir, sendQueuePack=self.sendQueuePack, logger=self.logger)

        self.currentTab = tab1
        self.lastCurrentTab = tab1
        self.eventTab = tab6
        self.autoAujTab = tab8
        self.DataTabWidget.currentChanged.connect(self.onTabSwitch)
        self.DataTabWidget.addTab(tab1, "测量监视")
        self.tabs.append(tab1)
        self.DataTabWidget.addTab(tab2, "通信及数据")
        self.tabs.append(tab2)
        self.DataTabWidget.addTab(tab3, "开入开出")
        self.tabs.append(tab3)
        self.DataTabWidget.addTab(tab4, "保护参数 1")
        self.tabs.append(tab4)
        self.DataTabWidget.addTab(tab5, "保护参数 2")
        self.tabs.append(tab5)
        self.DataTabWidget.addTab(tab6, "事件记录")
        self.tabs.append(tab6)
        self.DataTabWidget.addTab(tab7, "故障信息")
        self.tabs.append(tab7)
        if self.userType > 0:
            self.DataTabWidget.addTab(tab8, "厂家设置")
            self.tabs.append(tab8)
            self.DataTabWidget.addTab(tab9, "测试报告")
            self.tabs.append(tab9)
            self.DataTabWidget.addTab(tab10, "测量板测试")
            self.tabs.append(tab10)

    def dealUpdateWidget(self, changeAddrList):
        """接收数据时 需要处理的界面更新"""
        # self.currentTab.updateByTimer()
        self.currentTab.updateUnconditional()
        if self.currentTab is not self.eventTab:
            self.eventTab.updateUnconditional()
        self.currentTab.updateBySerialRecvBuff(changeAddrList)

    def isTabEnoughData(self, tabIdx):
        if tabIdx == 0:
            targetSample = (0x0023,)
        elif tabIdx == 1:
            targetSample = (0x0003, 0x0040, 0x0700, 0x0200, 0x0600)
        elif tabIdx == 2:
            targetSample = (0x0300, 0x0400)
        elif tabIdx == 3:
            targetSample = (0x0800, 0x040A)
        elif tabIdx == 4:
            targetSample = (0x0800, 0x040A)
        elif tabIdx == 6:
            targetSample = (0x0E40, 0x0009)
        elif tabIdx == 7:
            targetSample = (0x0E00, 0x000E)
        elif tabIdx == 8:
            targetSample = (0x000E, 0x0023)
        else:
            return False
        if any((self.serRecvDir.get(addr) is None for addr in targetSample)):
            return False
        else:
            return True

    def getSendQueue(self, tabIdx):
        try:
            if self.deviceType == 0:
                return mengy_spac800.TAB_SEND_QUEUE[tabIdx]
            else:
                return mengy_spac800_bt.TAB_SEND_QUEUE[tabIdx]
        except Exception:
            self.logger.exception("get send queue eexception")
            return None

    def onTabSwitch(self):
        """切换tab"""
        self.currentTab = self.DataTabWidget.currentWidget()
        self.logger.info("tab change to {} ".format(self.currentTab.objectName()))
        if isinstance(self.currentTab, Tab_10_IDIN201F) and (self.lastCurrentTab is not self.currentTab):
            choice = QMessageBox.question(self, "警告", "切换通信协议 SPAC800 --> IDIN201F ?", QMessageBox.Yes | QMessageBox.No)
            if choice == QMessageBox.Yes:
                for _ in range(6):
                    try:
                        self.sendQueue.get(timeout=0.5)
                    except Exception:
                        break
                self.sendQueuePack(funCode=0x10, start=0x0602, data=(2,))  # 修改串口1规约
                self.sendQueue.put(IDIN201F_READ_QUEUE)
                self.serRecvDir.clear()
            else:
                self.currentTab = self.lastCurrentTab
                self.DataTabWidget.blockSignals(True)
                self.DataTabWidget.setCurrentWidget(self.lastCurrentTab)
                self.DataTabWidget.blockSignals(False)

        if isinstance(self.lastCurrentTab, Tab_10_IDIN201F) and (self.lastCurrentTab is not self.currentTab):
            choice = QMessageBox.question(
                self,
                "警告",
                "手动切换通信协议 IDIN201F --> SPAC800 ?\n参数设置-通信参数-串口\n规约1 内部调试 --> Modbus",
                QMessageBox.Yes | QMessageBox.No,
            )
            if choice == QMessageBox.Yes:
                self.serRecvDir.clear()
            else:
                self.currentTab = self.lastCurrentTab
                self.DataTabWidget.blockSignals(True)
                self.DataTabWidget.setCurrentWidget(self.lastCurrentTab)
                self.DataTabWidget.blockSignals(False)

        if self.currentTab in self.tabs and (not isinstance(self.currentTab, Tab_10_IDIN201F)):
            tabIdx = self.tabs.index(self.currentTab)
            if tabIdx < len(self.tabs):
                self.sendQueue.put(self.getSendQueue(tabIdx))
                if isinstance(self.lastCurrentTab, Tab_10_IDIN201F):
                    self.serRecvDir.clear()
            if self.isTabEnoughData(tabIdx):
                self.currentTab.updateWhole()
            else:
                QTimer.singleShot(1500, self.currentTab.updateWhole)
        self.lastCurrentTab = self.currentTab


if __name__ == "__main__":
    if PLATFORM_RELEASE == "XP":
        logfile_path = "./logs/serial.log"
        basedir = os.path.dirname(os.path.abspath(logfile_path))
        os.makedirs(basedir, exist_ok=True)
        logger = logzero.setup_logger("spac800", logfile=logfile_path, backupCount=10, maxBytes=2 ** 20)
        # logger.removeHandler(logger.handlers[0])
    else:
        logger = logzero.setup_logger("spac800", logfile=None)
    app = QApplication(sys.argv)
    app.setStyle("Windows")
    spac800 = SPAC800(app=app, logger=logger)
    app.aboutToQuit.connect(spac800.cleanUp)
    sys.exit(app.exec_())
